/*
 *  GMAP3 Plugin for JQuery 
 *  Version   : 2.0
 *  Date      : January, 07 2011
 *  Licence   : GPL v3 : http://www.gnu.org/licenses/gpl.html  
 *  Author    : DEMONTE Jean-Baptiste
 *  Contact   : jbdemonte@gmail.com
 *  Web site  : http://night-coder.com
 *  
 *  Thanks for mailing me for bug, feedback, integration ...
 *
 *  2.0
 *    updated: jQuery used instead of $ 
 *    updated: callback function return jquery object instead of it's id
 *    updated: events return jquery object instead of it's id
 *    updated: infoWindow simplier in :addMarker
 *    updated: latLng standardised and can now be [lat:number, lng:number]
 *    updated: callback can be an array of function
 *    updated: setbicyclinglayer renamed addBicyclingLayer
 *    updated: setGroundOverlay renamed addGroundOverlay
 *    updated: setkmllayer renamed addkmllayer
 *    updated: setTrafficLayer renamed addTrafficLayer
 *    added: manage callback in :init
 *    added: onces : addListenerOnce managed
 *    added: :addOverlay :addFixPanel :addCircle :addRectangle :getElevation :removeBicyclingLayer :removeTrafficLayer
 *    fixed: :init run by _subcall doesn't acknoledge stack  
 *    fixed: :addDirectionRenderer twice no longer remove newt action
 *    fixed: :addPolygon and :addPolyline now return the elements
 *    fixed: :addMarker run apply (deprecated methods removed)
 *    fixed: :removeDirectionsRenderer remove now the directions 
 *    fixed: some bugs when wrong parameters (locks)
 *    fixed: private function manage auto init like public one ( _getLatLng => :getLatLng )  
 *   
 *  1.2 - 2010-12-03
 *    fixed : map modification in frm functions (addmarker...) now works
 *    fixed : asynchronous actions (ie: address resolution) were bypassed by synchronous
 *            ie: addMarker[string address], enableScrollWheelZoom => before address is resolved, enableScrollWheelZoom starts
 *            => added a stack manager which push all actions and start next one once previous is finished.
 *            thanks to james for bug report 
 *    added : addStyledMap, setStyledMap      
 *         
 *  1.1 - 2010-11-10
 *    fixed : implicit init doesn't use map parameters
 *    added : getRoute, addDirectionsRenderer, setDirectionsPanel, setDirections
 *  
 *  1.0 - 2010-11-01      
 */
  
jQuery.gmap3 = {
  /*============================*/
  /*          PRIVATE           */
  /*============================*/


  _ids:{},
  
  /****************************************************************************/
  /*                                  STACK
  /****************************************************************************/
  _running:{
  },
  _stack:{
    _a:{},
    init: function(id){
      if (!this._a[id]) this._a[id] = [];
    },
    add: function(id, a){
      this.init(id);
      this._a[id].push(a);
    },
    addNext: function(id, a){
      this.init(id);
      var t = [], i=0;
      for(var k in this._a[id]){
        if (i == 1) t.push(a);
        t.push(this._a[id][k]);
        i++;
      }
      if (i < 2) t.push(a);
      this._a[id] = t;
    },
    get: function(id){
      if (this._a[id]) 
        for(var k in this._a[id]){
          if (this._a[id][k]) return this._a[id][k];
        }
      return false;
    },
    ack: function(id){
      if (this._a[id]) 
        for(var k in this._a[id]){                     
          if (this._a[id][k]) {
            delete this._a[id][k];
            break;
          }
        }
        if (this.empty(id)) delete this._a[id];
    },
    empty: function(id){
      if (!this._a[id]) return true;    
      for(var k in this._a[id]){
        if (this._a[id][k]) return false
      }
      return true;
    }
    
  },
  /**
   * @desc create default structure if not existing
   **/
  _init: function($this, id){
    if (!this._ids[ id ]) {
      this._ids[ id ] = {
        $this:$this,
        styles: {},
        map:null,
        dr:null
      };
    }
  },
  /**
   * @desc store actions to do in a stack manager
   **/
  _plan: function($this, id, list){
    this._init($this, id);
    for(var k in list) this._stack.add(id, list[k] );
    this._run(id);
  },
  /**
   * @desc store one action to do in a stack manager after the first
   **/
  _planNext: function(id, a){
    var $this = this._jObject(id);
    this._init($this, id);
    this._stack.addNext(id, a);
  },
  /**
   * @desc called when action in finished, to acknoledge the current in stack and start next one
   **/
  _end: function(id){
    delete this._running[id];
    this._stack.ack(id);
    this._run(id);
  },
  /**
   * @desc if not running, start next action in stack
   **/
  _run: function(id){
    if (this._running[id]) return;
    var a = this._stack.get(id);
    if (!a) return;
    this._running[id] = true;
    this._proceed(id, a);
  },
  /****************************************************************************/
  
  _properties:['events','onces','options','apply', 'callback'],
  
  _default:{
    verbose:true,
    init:{
      mapTypeId : google.maps.MapTypeId.ROADMAP,
      center:[46.593623,0.342922],
      zoom: 10
    }
  },
  
  _geocoder: null,
  _getGeocoder: function(){
    if (!this._geocoder) this._geocoder = new google.maps.Geocoder();
    return this._geocoder;
  },
  
  _directionsService: null,
  _getDirectionsService: function(){
    if (!this._directionsService) this._directionsService = new google.maps.DirectionsService();
    return this._directionsService;
  },
  
  _elevationService: null,
  _getElevationService: function(){
    if (!this._elevationService) this._elevationService = new google.maps.ElevationService();
    return this._elevationService;
  },
  
  _getMap: function( id ){
    return this._ids[ id ].map;
  },
  
  _setMap: function (id, map){
    this._ids[ id ].map = map;
  },
  
  _jObject: function( id ){
    return this._ids[ id ].$this;
  },
  
  _addStyle: function(id, styleId, style){
    this._ids[ id ].styles[ styleId ] = style;
  },
  
  _getStyles: function(id){
    return this._ids[ id ].styles;
  },
  
  _getStyle: function(id, styleId){
    return this._ids[ id ].styles[ styleId ];
  },
  
  _styleExist: function(id, styleId){
    return this._ids[ id ] && this._ids[ id ].styles[ styleId ];
  },
  
  _getDirectionRenderer: function(id){
    return this._ids[ id ].dr;
  },
  
  _setDirectionRenderer: function(id, dr){
    this._ids[ id ].dr = dr;
  },
  
  _exist: function(id){
    return this._ids[ id ].map ? true : false;
  },
  
  /**
   * @desc return true if "init" action must be run
   **/
  _autoInit: function(name){
    if ( !name || ( (name[0] != ':') && (name[0] != '_') ) ) return true;
    var names = [ 'init', 
                  'geolatlng', 
                  'getlatlng', 
                  'getroute',
                  'getelevation', 
                  'addstyledmap', 
                  'destroy'];
    name = name.substr(1); // remove ':' & '_' : public & private
    for(var k in names){
      if (names[k] == name) return false;
    }
    return true;
  },
  /**
   * @desc call functions associated
   * @param
   *  id      : string
   *  action  : string : function wanted
   *     
   *  options : {}
   *     
   *    O1    : {}
   *    O2    : {}
   *    ...
   *    On    : {}
   *      => On : option : {}
   *          action : string : function name
   *          ... (depending of functions called)
   *             
   *  args    : [] : parameters for directs call to map
   *  target? : object : replace map to call function 
   **/
  _proceed: function(id, data){
    data = data || {};
    var action = data['action'] || ':init';
    var iaction = action.toLowerCase();
    var fl = action.substr(0,1);
    if (fl == '_') return; // private function
    var ok = true;
    if ( !this._exist(id) && this._autoInit(iaction) ){
      this.init(id, jQuery.extend({}, this._default['init'], data['args'] && data['args']['map'] ? data['args']['map'] : data['map'] ? data['map'] : {}), true);
    }
    if (fl == ':'){
      // framework functions
      action = iaction.substr(1);
      if (typeof(this[action]) == 'function'){ 
        this[action](id, $.extend({}, this._default[action], data['args'] ? data['args'] : data)); // call fnc and extends defaults data
      } else ok = false;
    } else {
      // target of a direct call
      if (data['target']){
        if (typeof(data['target'][action]) == 'function'){
          data['out'] = data['target'][action].apply(data['target'], data['args'] ? data['args'] : []);
        } else ok = false;
      // gm direct function :  no result so not rewrited, directly wrapped using array "args" as parameters (ie. setOptions, addMapType, ...)
      } else {
        var map = this._getMap(id);
        if (typeof(map[action]) == 'function'){
          data['out'] = map[action].apply(map, data['args'] ? data['args'] : [] );
        } else ok = false;
      }
      if (!ok && this._default['verbose']) alert("unknown action : " + action);
      this._callback(id, data['out'], data);
      this._end(id);
    }
  },
  
  /**
   * @desc call a function of framework or google map object of the instance
   * @param
   *  id      : string : instance
   *  fncName : string : function name
   *  ... (depending on function called)
   **/
  _call: function(/* id, fncName [, ...] */){
    if (arguments.length < 2) return;
    if (!this._exist(arguments[0])) return ;
    var id = arguments[0],
        fname = arguments[1];
    var map = this._getMap(id);
    if (typeof(map[ fname ]) != 'function') return;
    var args = [];
    for(var i=2; i<arguments.length; i++){
      args.push(arguments[i]);
    }
    return map[ fname ].apply( map, args );
  },
  /**
   * @desc convert data to array
   **/
  _array: function(mixed){
    var a = [];
    for(var k in mixed) a.push(mixed[k]);
    return a;
  },
  
  /**
   * @desc init if not and manage map subcall (zoom, center)
   **/
  _subcall: function(id, data, latLng){
    if (!data['map']) return;
    if (!this._exist(id))
        this.init(id, jQuery.extend({}, data['map'], {center:latLng}), true);
    else { 
        if (data['map']['center']) this._call(id, "setCenter", latLng);
        if (typeof(data['map']['zoom']) != 'undefined') this._call(id, "setZoom", data['map']['zoom']);
        if (typeof(data['map']['mapTypeId'])!= 'undefined') this._call(id, "setMapTypeId", data['map']['mapTypeId']);
    }
  },
  
  /**
   * @desc attach an event to a sender (once) 
   **/
  _attachEvent: function(id, sender, name, f, once){
    var $o = this._jObject(id);
    google.maps.event['addListener'+(once?'Once':'')](sender, name, function(event) {
      f($o, sender, event);
    });
  },
  
  /**
   * @desc attach events from a container to a sender 
   * ctnr[
   *  events => { eventName => function, }
   *  onces  => { eventName => function, }      
   * ]
   **/
  _attachEvents : function(id, sender, ctnr){
    if (!ctnr) return
    if (ctnr['events']){
      for(var name in ctnr['events']){
        if (typeof(ctnr['events'][name]) == 'function'){
          this._attachEvent(id, sender, name, ctnr['events'][name], false);
        }
      }
    }
    if (ctnr['onces']){
      for(var name in ctnr['onces']){
        if (typeof(ctnr['onces'][name]) == 'function'){
          this._attachEvent(id, sender, name, ctnr['onces'][name], true);
        }
      }
    }
  },
  
  /**
   * @desc execute callback functions 
   **/
  _callback: function(mixed, result, ctnr){
    ctnr['out'] = result;
    if (typeof(ctnr['callback']) == 'function') {
      var $j = typeof(mixed) == 'string' ? this._jObject(mixed) : mixed;
      ctnr['callback']($j, result);
    } else if (typeof(ctnr['callback']) == 'object') {
      var $j;
      for(var k in ctnr['callback']){
        if (!$j) $j = typeof(mixed) == 'string' ? this._jObject(mixed) : mixed;
        if (typeof(ctnr['callback'][k]) == 'function') ctnr['callback'][k]($j, result);
      }
    }
  },
  
  /**
   * @desc execute end functions 
   **/
  _manageEnd: function(id, sender, data, internal){
    if (typeof(sender) == 'object'){
      this._attachEvents(id, sender, data);
      for(var k in data['apply']){
        var c = data['apply'][k];
        if(!c['action']) continue;
        if (typeof(sender[c['action']]) != 'function') continue;
        if (c['args']) {
          sender[c['action']].apply(sender, c['args']);
        } else {
          sender[c['action']]();
        }
      }
    }
    if (!internal) {
      this._callback(id, sender, data);
      this._end(id);
    }
  },
  
  /**
   *  @desc convert mixed [ lat, lng ] objet by google.maps.LatLng
   **/
  _latLng: function(mixed, emptyReturnMixed, noFlat){
    if (!mixed) return null;
    if (mixed['latLng']) {
      return this._latLng(mixed['latLng']);
    }
    var empty = emptyReturnMixed ? mixed : null;
    if (typeof(mixed['lat']) == 'function') {
      return mixed;
    } else if (typeof(mixed['lat']) == 'number') {
      return new google.maps.LatLng(mixed['lat'], mixed['lng']);
    } else if ( !noFlat ){
      var latLng={}, i=0;
      for(var k in mixed){
        if (typeof(mixed[k]) != 'number') return empty;
        latLng[i==0?'lat':'lng'] = mixed[k];
        if (i) break;
        i++;
      }
      if (i) return new google.maps.LatLng(latLng['lat'], latLng['lng']);
    }
    return empty;
  },
  
  _count: function(mixed){
    var c = 0;
    for(var k in mixed) c++;
    return c;
  },
  
  /**
   * @desc convert mixed [ sw, ne ] object by google.maps.LatLngBounds
   **/
  _latLngBounds: function(mixed, flatAllowed, emptyReturnMixed){
    if (!mixed) return null;
    var empty = emptyReturnMixed ? mixed : null;
    if (typeof(mixed['getCenter']) == 'function') return mixed;
    var cnt = this._count(mixed);
    if (cnt == 2){
      var ne, sw;
      if (mixed['ne'] && mixed['sw']){
        ne = this._latLng(mixed['ne']);
        sw = this._latLng(mixed['sw']);
      } else {
        for(var k in mixed){
          if (!ne) {
            ne = this._latLng(mixed[k]);
          } else {
            sw = this._latLng(mixed[k]);
          }
        }
      }
      if (sw && ne) return new google.maps.LatLngBounds(sw, ne);
      return empty;
    } else if (cnt == 4){
      var t = ['n', 'e', 's', 'w'], ok=true;
      for(i in t) ok &= typeof(mixed[t[i]]) == 'number';
      if (ok) return new google.maps.LatLngBounds(this._latLng([mixed['s'], mixed['w']]), this._latLng([mixed['n'], mixed['e']]));
      if (flatAllowed){
        var i=0, nesw={};
        for(var k in mixed){
          if (typeof(mixed[k]) != 'number') return empty;
          nesw[t[i]] = mixed[k];
          i++;
        }
        return new google.maps.LatLngBounds(this._latLng([nesw['s'], nesw['w']]), this._latLng([nesw['n'], nesw['e']]));
      }
    }
    return empty;
  },
  
  /**
   * @desc search an (insensitive) key
   **/
  _ikey: function(object, key){
    key = key.toLowerCase();
    for(var k in object){
      if (k.toLowerCase() == key) return k;
    }
    return false;
  },
  
  /**
   * @desc search an (insensitive) key
   **/
  _ival: function(object, key){
    var k = this._ikey(object, key);
    if ( k ) return object[k];
    return null;
  },
  
  /**
   * @desc return true if at least one key is set in object
   * nb: keys in lowercase
   **/
  _hasKey: function(object, keys){
    if (!object || !keys) return false;
    for(var n in object){
      n = n.toLowerCase();
      for(var k in keys){
        if (n == keys[k]) return true;
      }
    }
    return false;
  },
  
  /**
   * @desc return a standard object
   * nb: include in lowercase
   **/
  _extractObject: function(data, include){
    if (this._hasKey(data, this._properties) || this._hasKey(data, include)){
      var k,p,ip,r={};
      for(k in this._properties){
        p=this._properties[k];
        ip = this._ikey(data, p);
        r[p] = ip ? data[ip] : {};
      }
      for(k in include){
        p=include [k];
        ip = this._ikey(data, p);
        r[p] = ip ? data[ip] : {};
      }
      return r;
    } else {
      r = { options : {} };
      for(k in data){
        if (k == 'action') continue;
        r.options[k] = data[k];
      }
      return r;
    }
  },
  
  /**
   * @desc identify object from object list or parameters list : [ objectName:{data} ] or [ otherObject:{}, ] or [ object properties ]
   * nb: include, exclude in lowercase
   **/
  _object: function(name, data, include, exclude){
    var k = this._ikey(data, name);
    if (k) return this._extractObject(data[k], include);
    var p, r = {}, keys=['map'];
    for(k in exclude) keys.push(exclude[k]);
    if (!this._hasKey(data, keys)) r = this._extractObject(data, include);
    for(k in this._properties){
      p=this._properties[k];
      if (!r[p]) r[p] = {};
    }
    return r;
  },
  
  /**
   * @desc Returns the geographical coordinates from an address and call internal method
   **/
  _resolveLatLng: function(id, data, method, all){
    var address = this._ival(data, 'address');
    if ( address ){
        var that = this;
        var callback = function(results, status) {
        if (status == google.maps.GeocoderStatus.OK){
          that[method](id, data, all ? results : results[0].geometry.location);
        } else {
          that[method](id, data, false);
        }
      };
      this._getGeocoder().geocode( { 'address': address }, callback );
    } else {
      this[method](id, data, this._latLng(data, false, true));
    }
  },
  
  /*============================*/
  /*          PUBLIC            */
  /*============================*/
  
  /**
   * @desc Destroy an existing instance
   **/
  destroy: function(id, data){
    if (this._ids[ id ]){
        this._ids[ id ].$this.empty();
        if (this._ids[ id ].dr) delete this._ids[ id ].dr;
        if (this._ids[ id ].bl) delete this._ids[ id ].bl;
        if (this._ids[ id ].tl) delete this._ids[ id ].tl;
        for(var k in this._ids[ id ].styles){
          delete this._ids[ id ].styles[ k ];
        }
        delete this._ids[ id ].map;
        var $j = this._jObject(id);
        delete this._ids[ id ];
        this._callback($j, null, data);
    }
    this._end(id);
  },
  
  /**
   * @desc Initialize google map object an attach it to the dom element (using id)
   **/
  init: function(id, data, internal){
    if ( (id == '') || (this._exist(id)) ) return this._end(id);
    var o = this._object('map', data);
    if ( (typeof(o['options']['center']) == 'boolean') && o['options']['center']) return false; // wait for an address resolution
    var opts = jQuery.extend({}, this._default['init'], o['options']);
    if (!opts['center']) opts['center'] = [this._default.init['center']['lat'], this._default.init['center']['lng']];
    opts['center'] = this._latLng(opts['center']);
    
    this._setMap(id, new google.maps.Map(document.getElementById(id), opts));
    var map = this._getMap(id);
    
    // add previous added styles
    var styles = this._getStyles( id );
    for(var k in styles) map.mapTypes.set(k, styles[k]);
    
    this._manageEnd(id, map, o, internal);
    return true;
  },
  
  /**
   * @desc Returns the geographical coordinates from an address
   **/
  getlatlng: function(id, data){
    this._resolveLatLng(id, data, '_getLatLng', true);
  },
  _getLatLng: function(id, data, results){
    this._manageEnd(id, results, data);
  },
  
  /**
   * @desc Return a route
   **/
  getroute: function(id, data){
    if ( (typeof(data['callback']) == 'function') && data['options'] ) {
      data['options']['origin']      = this._latLng(data['options']['origin'], true);
      data['options']['destination'] = this._latLng(data['options']['destination'], true);
      var $this = this._jObject(id);
      var callback = function(results, status) {
        data['out'] = status == google.maps.DirectionsStatus.OK ? results : false;
        data['callback']($this, data['out']);
      };
      this._getDirectionsService().route( data['options'], callback );
    }
    this._end(id);
  },
  
  getelevation: function(id, data){
    var cb = this._ival(data, 'callback');
    if (cb && typeof(cb) == 'function') {
      var $this = this._jObject(id);
      var callback = function(results, status) {
        data['out'] = status == google.maps.ElevationStatus.OK ? results : false;
        data['callback']($this, data['out']);
      };
      var locations = [];
      var latLng = this._ival(data, 'latlng')
      if ( latLng ) 
        locations.push( this._latLng(latLng) )
      else {
        var ls = this._ival(data, 'locations')
        if ( ls ){
          for(var k in ls){
            locations.push( this._latLng(ls[k]) );
          }
        }
      }
      if ( locations.length ){
        this._getElevationService().getElevationForLocations( {locations:locations}, callback );
      } else {
        var path = this._ival(data, 'path');
        var samples = this._ival(data, 'samples');
        if (path && samples){
          for(var k in path){
            locations.push( this._latLng(path[k]) );
          }
          if ( locations.length ){
            this._getElevationService().getElevationAlongPath( {path:locations, samples:samples}, callback );
          }
        }
      }
    }
    this._end(id);
  },
  
  /**
   * @desc Add a marker to a map after address resolution
   * if [infowindow] add an infowindow attached to the marker   
   **/
  addmarker: function(id, data){
    this._resolveLatLng(id, data, '_addMarker');
  },
  _addMarker: function(id, data, latLng ){
    if (!latLng) return this._end(id);
    this._subcall(id, data, latLng);
    
    var o = this._object('marker', data);
    o['options']['position'] = latLng;
    o['options']['map'] = this._getMap(id);
    var marker = new google.maps.Marker(o['options']);
    
    if ( data['infowindow'] ){
      var oi = this._object('infowindow', data['infowindow'], ['open']);
      if ( (typeof(oi['open']) == 'undefined') || oi['open'] ) {
        oi['apply'] = this._array(oi['apply']);
        oi['apply'].unshift({action:'open', args:[this._getMap(id), marker]});
      }
      oi['action'] = ':addInfoWindow';
      this._planNext(id, oi); 
    }
    
    this._manageEnd(id, marker, o);
  },
  
  /**
   * @desc Add an infowindow after address resolution
   **/
  addinfowindow: function(id, data){ 
    this._resolveLatLng(id, data, '_addInfoWindow');
  },
  _addInfoWindow: function(id, data, latLng){
    this._subcall(id, data, latLng);
    var o = this._object('infowindow', data, ['open']);
    if (latLng) o['options']['position'] = latLng;
    var infowindow = new google.maps.InfoWindow(o['options']);
    if ( (typeof(o['open']) == 'undefined') || o['open'] ){
      o['apply'] = this._array(o['apply']);
      o['apply'].unshift({action:'open', args:[this._getMap(id)]});
    }
    this._manageEnd(id, infowindow, o);
  },
  
  /**
   * @desc add a polygone / polylin on a map
   **/
  addpolyline: function(id, data){
    this._addPoly(id, data, 'Polyline', 'path');
  },
  addpolygon: function(id, data){
    this._addPoly(id, data, 'Polygon', 'paths');
  },
  _addPoly: function(id, data, poly, path){
    var o = this._object(poly.toLowerCase(), data, [path]);
    if (o[path]){
      o['options'][path] = [];
      var i = 0; 
      for(var k in o[path]){
        o['options'][path][i++] = this._latLng(o[path][k]);
      }
    }
    var poly = new google.maps[poly](o['options']);
    poly.setMap(this._getMap(id));
    this._manageEnd(id, poly, o);
  },
  
  /**
   * @desc add a circle   
   **/
  addcircle: function(id, data){
    this._resolveLatLng(id, data, '_addCircle');
  },
  _addCircle: function(id, data, latLng ){
    var o = this._object('circle', data);
    if (!latLng) latLng = this._latLng(o['options']['center']);
    if (!latLng) return this._end(id);
    this._subcall(id, data, latLng);
    var o = this._object('circle', data);
    o['options']['center'] = latLng;
    o['options']['map'] = this._getMap(id);
    var circle = new google.maps.Circle(o['options']);
    this._manageEnd(id, circle, o);
  },
  
  /**
   * @desc add a rectangle   
   **/
  addrectangle: function(id, data){
    this._resolveLatLng(id, data, '_addRectangle');
  },
  _addRectangle: function(id, data, latLng ){
    var o = this._object('rectangle', data);
    o['options']['bounds'] = this._latLngBounds(o['options']['bounds'], true);
    if (!o['options']['bounds']) return this._end(id);
    var center = o['options']['bounds'].getCenter();
    this._subcall(id, data, center);
    o['options']['map'] = this._getMap(id);
    var rectangle = new google.maps.Rectangle(o['options']);
    this._manageEnd(id, rectangle, o);
  },
  
  /**
   * @desc add an overlay to a map after address resolution
   **/
  addoverlay: function(id, data){
    this._resolveLatLng(id, data, '_addOverlay');
  },
  _addOverlay: function(id, data, latLng){
    var o = this._object('overlay', data);
    var opts =  jQuery.extend({
                  pane: 'floatPane',
                  content: '',
                  offset:{
                    x:0,y:0
                  }
                },
                o['options']);
  
    f.prototype = new google.maps.OverlayView();
    
    function f(opts, latLng, map) {
      this.opts_ = opts;
      this.$div_ = null;
      this.latLng_ = latLng;
      this.map_ = map;
      this.setMap(map);
    }
    f.prototype.onAdd = function() {
      var $div = $('<div></div>');
      $div
        .css('border', 'none')
        .css('borderWidth', '0px')
        .css('position', 'absolute');
      $div.append($(this.opts_['content']));
      this.$div_ = $div;
      var panes = this.getPanes();
      if (panes[this.opts_['pane']]) $(panes[this.opts_['pane']]).append(this.$div_);
    }
    f.prototype.draw = function() {
      if (!this.$div_) return;
      var overlayProjection = this.getProjection();
      var ps = overlayProjection.fromLatLngToDivPixel(this.latLng_);
      this.$div_
        .css('left', (ps.x+this.opts_['offset']['x']) + 'px')
        .css('top' , (ps.y+this.opts_['offset']['y']) + 'px');
    }
    f.prototype.onRemove = function() {
      this.$div_.remove();
      this.$div_ = null;
    }
    f.prototype.hide = function() {
      if (this.$div_) this.$div_.hide();
    }
    f.prototype.show = function() {
      if (this.$div_) this.$div_.show();
    }
    f.prototype.toggle = function() {
      if (this.$div_) {
        if (this.$div_.is(':visible')){
          this.show();
        } else {
          this.hide();
        }
      }
    }
    f.prototype.toggleDOM = function() {
      if (!this.$div_) return;
      if (this.getMap()) {
        this.setMap(null);
      } else {
        this.setMap(this.map_);
      }
    }
    var overlay = new f(opts, latLng, this._getMap(id));
    this._manageEnd(id, overlay, o);
  },
  
  /**
   * @desc add fixed panel to a map
   **/
  addfixpanel: function(id, data){
    var o = this._object('fixpanel', data);
    if (o['options']['content']){
      var x=0, y=0;
      var $c = $(o['options']['content']);
      
      if (typeof(o['options']['left']) != 'undefined'){
        x = o['options']['left'];
      } else if (typeof(o['options']['right']) != 'undefined'){
        x = this._jObject(id).width() - $c.width() - o['options']['right'];
      } else if (o['options']['center']){
        x = (this._jObject(id).width() - $c.width()) / 2;
      }
      
      if (typeof(o['options']['top']) != 'undefined'){
        y = o['options']['top'];
      } else if (typeof(o['options']['bottom']) != 'undefined'){
        y = this._jObject(id).height() - $c.height() - o['options']['bottom'];
      } else if (o['options']['middle']){
        y = (this._jObject(id).height() - $c.height()) / 2
      }
    
      var $div = 
        $('<div></div>')
          .css('position', 'absolute')
          .css('top', y+'px')
          .css('left', x+'px')
          .css('z-index', '1000')
          .append(o['options']['content']);
      
      this._jObject(id).first().prepend($div);
      this._attachEvents(id, this._getMap(id), o);
      this._callback(id, $div, o);
    }
    this._end(id);
  },
  
  /**
   * @desc Remove a direction renderer
   **/
  removedirectionsrenderer: function(id, data, internal){
    var o = this._object('directionrenderer', data);
    if (this._ids[ id ].dr) {
      this._ids[ id ].dr.setMap(null);
      delete this._ids[ id ].dr;
    }
    this._manageEnd(id, true, o, internal);
  },
  
  /**
   * @desc Add a direction renderer to a map
   **/
  adddirectionsrenderer: function(id, data, internal){
    var o = this._object('directionrenderer', data, ['panelId']);
    this.removedirectionsrenderer(id, o, true);
    o['options']['map'] = this._getMap(id);
    this._setDirectionRenderer(id, new google.maps.DirectionsRenderer(o['options']));
    var dr = this._getDirectionRenderer(id);
    if (o['panelId']) dr.setPanel(document.getElementById(o['panelId']));
    this._manageEnd(id, dr, o, internal);
  },
  
  /**
   * @desc Set direction panel to a dom element from it ID
   **/
  setdirectionspanel: function(id, data){
    var o = this._object('directionpanel', data, ['id']);
    var dr;
    if (o['id']) {
      dr = this._getDirectionRenderer(id);
      dr.setPanel(document.getElementById(o['id']));
    }
    this._manageEnd(id, dr, o);
  },
  
  /**
   * @desc Set directions on a map (create Direction Renderer if needed)
   **/
  setdirections: function(id, data){
    var o = this._object('directions', data);
    if (data) o['options']['directions'] = data['directions'] ? data['directions'] : (data['options'] && data['options']['directions'] ? data['options']['directions'] : null);
    var dr;
    if (o['options']['directions']) {
      dr = this._getDirectionRenderer(id);
      if (!dr) {
        this.adddirectionsrenderer(id, o, true);
        dr = this._getDirectionRenderer(id);
      } else {
        dr.setDirections(o['options']['directions']);
      }
    }
    this._manageEnd(id, dr, o);
  },
  
  /**
   * @desc set a streetview to a map
   **/
  setstreetview: function(id, data){
    var o = this._object('streetview', data, ['id']);
    var panorama = new google.maps.StreetViewPanorama(document.getElementById(o['id']),o['options']);
    this._getMap(id).setStreetView(panorama);
    this._manageEnd(id, panorama, o);
  },
  
  /**
   * @desc add a kml layer to a map
   **/
  addkmllayer: function(id, data){
    var o = this._object('kmllayer', data, ['url']);
    o['options']['map'] = this._getMap(id);
    var kml = new google.maps.KmlLayer(o['url'], o['options']);
    this._manageEnd(id, kml, data);
  },
  
  /**
   * @desc add a traffic layer to a map
   **/
  addtrafficlayer: function(id, data){
    var o = this._object('trafficlayer');
    if (!this._ids[ id ].tl){
      this._ids[ id ].tl = new google.maps.TrafficLayer();
      this._ids[ id ].tl.setMap(this._getMap(id));
    }
    this._manageEnd(id, this._ids[ id ].tl, o);
  },
  
  /**
   * @desc remove a traffic layer from a map
   **/
  removetrafficlayer: function(id, data){
    var o = this._object('trafficlayer');
    var r = typeof(this._ids[ id ].tl) != 'undefined';
    if (this._ids[ id ].tl){
      this._ids[ id ].tl.setMap(null);
      delete this._ids[ id ].tl;
    }
    this._manageEnd(id, r, o);
  },
  
  /**
   * @desc set a bicycling layer to a map
   **/
  addbicyclinglayer: function(id, data){
    var o = this._object('bicyclinglayer');
    if (!this._ids[ id ].bl){
      this._ids[ id ].bl = new google.maps.BicyclingLayer();
      this._ids[ id ].bl.setMap(this._getMap(id));
    }
    this._manageEnd(id, this._ids[ id ].bl, o);
  },
  
  /**
   * @desc remove a bicycling layer from a map
   **/
  removebicyclinglayer: function(id, data){
    var o = this._object('bicyclinglayer');
    var r = typeof(this._ids[ id ].bl) != 'undefined';
    if (this._ids[ id ].bl){
      this._ids[ id ].bl.setMap(null);
      delete this._ids[ id ].bl;
    }
    this._manageEnd(id, r, o);
  },
  
  
  /**
   * @desc add a ground overlay to a map
   **/
  addgroundoverlay: function(id, data){
    var o = this._object('groundoverlay', data, ['bounds', 'url']);
    var overlay;
    o['bounds'] = this._latLngBounds(o['bounds']);
    if (o['bounds'] && o['url']){
        overlay = new google.maps.GroundOverlay(o['url'], o['bounds']);
        overlay.setMap(this._getMap(id));
    }
    this._manageEnd(id, overlay, o);
  },
  
  /**
   * @desc Geolocalise the user and return a LatLng
   **/
  geolatlng: function(id, data){
    if (typeof(data['callback']) == 'function') {
      var $this = this._jObject(id);
      if(navigator.geolocation) {
        browserSupportFlag = true;
        navigator.geolocation.getCurrentPosition(function(position) {
          data['out'] = new google.maps.LatLng(position.coords.latitude,position.coords.longitude);
          data['callback']($this, data['out']);
        }, function() {
          data['out'] = false;
          data['callback']($this, data['out']);
        });
      } else if (google.gears) {
        browserSupportFlag = true;
        var geo = google.gears.factory.create('beta.geolocation');
        geo.getCurrentPosition(function(position) {
          data['out'] = new google.maps.LatLng(position.latitude,position.longitude);
          data['callback']($this, data['out']);
        }, function() {
          data['out'] = false;
          data['callback']($this, data['out']);
        });
      } else {
          data['out'] = false;
          data['callback']($this, data['out']);
      }
    }
    this._end(id);
  },
  
  /**
   * @desc Add a style to a map
   **/
  addstyledmap: function(id, data, internal){
    var o = this._object('styledmap', data, ['id', 'style']);
    var style;
    if  (o['style'] && o['id'] && !this._styleExist(id, o['id'])) {
      style = new google.maps.StyledMapType(o['style'], o['options']);
      this._addStyle(id, o['id'], style);
      if (this._getMap(id)) this._getMap(id).mapTypes.set(o['id'], style);
    }
    this._manageEnd(id, style, o, internal);
  },
  
  /**
   * @desc Set a style to a map (add it if needed)
   **/
  setstyledmap: function(id, data){
    var o = this._object('styledmap', data, ['id', 'style']);
    var style;
    if (o['id']) {
      this.addstyledmap(id, o, true);
      style = this._getStyle(id, o['id']);
      if (style) {
        this._getMap(id).setMapTypeId(o['id']);
        this._callback(id, style, data);
      }
    }
    this._manageEnd(id, style, o);
  },
  
  /**
   * @desc modify default values
   **/
  setDefault: function(d){
    for(var k in d){
      this._default[k] = jQuery.extend({}, this._default[k], d[k]);
    }
  }
};


jQuery.fn.extend({
  gmap3: function(){
    var $this = $(this);
    if ($this.length > 0){
      var id = $this.attr('id');
      var todo = [];
      for(var i=0; i<arguments.length; i++){
        todo.push(arguments[i] || {});
      }         
      if (!todo.length) todo.push({});
      jQuery.gmap3._plan($this, id, todo);
    }
    return $(this);
  }	
});